#include <iostream>
#include <fstream>
#include <string>
#include <cstdio>  // for getchar
#include <cstring> // for memset

#include "kglobal.h"
#include "kconfig.h"
#include "kcompile.h"
#include "kvm.h"
#include "kutils.h"

using namespace std;

bool compileOnly;  // -c
enum _INPUT_FILE_T
{
	SrcFile, // source file *.kim
	CmpFile  // compiled file *.kc
} inputFile;
string fileName;
string fileNameWoExt;

enum CommandOptions
{
	OPT_INVALID = -1,
	OPT_KIM_RUN = 0,
	OPT_KIM_CMP = 1,
	OPT_KC_RUN  = 2,
	OPT_NOTHING = 3
};

void ShowHelp();
CommandOptions ParseArguments(int argc, char* argv[]);
string GetExt(const string &fn);
string GetFileNameWoExt(const string &fn);

bool CompileFile();
bool CompileAndRun();
bool RunFile();
void Interact();

int main(int argc, char* argv[])
{
	switch (ParseArguments(argc, argv))
	{
	case OPT_INVALID:
		return -1;
	case OPT_KIM_RUN:
		// compile and run
		CompileAndRun();
		break;
	case OPT_KIM_CMP:
		// compile only
		if (CompileFile())
			cout << endl << "Compiled successfully to " << fileNameWoExt << endl;
		else
			cout << endl << "Compiled unsuccessfully." << endl;
		break;
	case OPT_KC_RUN:
		// run compiled file
		RunFile();
		break;
	case OPT_NOTHING:
		// nothing given, switch to interactive mode
		Interact();
		break;
	}

#if _DEBUG
	getchar();
#endif
	
#ifdef _MSC_VER
	_CrtDumpMemoryLeaks();
#endif
	return 0;
}

void ShowHelp()
{
	cout << "KimL compiler and interpreter - v1.0" << endl;
	cout << "KimL Specification 11.01" << endl;
	cout << "Usage: kiml [-c] [-d] [-h] file" << endl << endl;

	cout << "Options:" << endl;
	cout << "\t-c\tCompile but not run.\n\t\tOnly applicable for *.kim file." << endl;
	cout << "\t-d\tEmbed debug information in the compiled file.\n\t\tOnly applicable for *.kim file." << endl;
	cout << "\t-h\tShow help message." << endl << endl;
}

CommandOptions ParseArguments(int argc, char* argv[])
{
	bool hasFile = false;
	bool hasHelp = false;

	for (int i = 1; i < argc; ++i)
	{
		if (my_strequ(argv[i], "-c"))
		{
			if (!compileOnly)
			{
				compileOnly = true;
			}
			else
			{
				cout << "*** Duplicated option -c." << endl;
				return OPT_INVALID;
			}
		}
		else if (my_strequ(argv[i], "-d"))
		{
			if (!hasDebugInfo)
			{
				hasDebugInfo = true;
			}
			else
			{
				cout << "*** Duplicated option -d." << endl;
				return OPT_INVALID;
			}
		}
		else if (my_strequ(argv[i], "-h"))
		{
			if (!hasHelp)
			{
				hasHelp = true;
			}
			else
			{
				cout << "*** Duplicated option -h." << endl;
				return OPT_INVALID;
			}
		}
		else
		{
			if (!hasFile)
			{
				hasFile = true;
				fileName = argv[i];
				
				string ext = GetExt(fileName);
				
				if (ext.size() == 0)
				{
					cout << "*** Unknown file type." << endl;
					return OPT_INVALID;
				}
				
				if (my_strequ(ext.c_str(), "kim"))
				{
					inputFile = SrcFile;
					fileNameWoExt = GetFileNameWoExt(fileName);
				}
				else if (my_strequ(ext.c_str(), "kc"))
				{
					inputFile = CmpFile;
				}
				else
				{
					cout << "*** Unknown file type: " << ext << endl;
					return OPT_INVALID;
				}
			}
			else
			{
				cout << "*** Duplicated file." << endl;
				return OPT_INVALID;
			}
		}
	}

	if (hasHelp)
		ShowHelp();

	if (!hasFile)
	{
		if (!compileOnly && !hasDebugInfo)
		{
			return OPT_NOTHING;
		}
		else
		{
			cout << "*** Options given without a file." << endl;
			return OPT_INVALID;
		}
	}

	if (compileOnly)
	{
		if (inputFile == SrcFile)
		{
			return OPT_KIM_CMP;
		}
		else
		{
			cout << "*** Input file must be a *.kim file." << endl;
			return OPT_INVALID;
		}
	}
	else
	{
		if (inputFile == SrcFile)
			return OPT_KIM_RUN;
		else
			return OPT_KC_RUN;
	}
}

string GetExt(const string &fn)
{
	int len = fn.length();
	for (int i = len - 1; i >= 0; --i)
		if (fn[i] == '.' && i < len-1)
			return fn.substr(i+1);

	return string("");
}

string GetFileNameWoExt(const string &fn)
{
	int len = fn.length();
	for (int i = len - 1; i >= 0; --i)
		if (fn[i] == '.')
			return fn.substr(0, i);

	return string("");
}

bool CompileFile()
{
	static char magic[4] = { 'k', 'i', 'm', 'l' };
	static char ver[4] =   { '1', '1', '0', '1' };
	static char pad[8] = { 0 };

	ifstream ifs(fileName.c_str(), ios::binary);
	
	if (!ifs.is_open())
	{
		cout << "*** Can not open file: " << fileName << endl;
		return false;
	}

	int len;
	ifs.seekg(0, ios::end);
	len = ifs.tellg();
	ifs.seekg(0, ios::beg);

	char *src = new char[len + 1];
	memset(src, 0, len + 1);
	ifs.read(src, len);
	ifs.close();

	if (!Compile(src))
	{
		delete[] src;
		return false;
	}

	delete[] src;

	ofstream ofs(fileNameWoExt.append(".kc").c_str(), ios::binary);

	if (!ofs.is_open())
	{
		cout << "*** Can not write binary file: " << fileNameWoExt << endl;
		bytecode.CleanUp();
		strPool.CleanUp();
		return false;
	}

	// Header
	ofs.write(magic, sizeof(magic));
	ofs.write(ver, sizeof(ver));
	ofs.write(pad, sizeof(pad));

	// Bytecode section
	const unsigned char *bc = bytecode.GetStream();
	ofs.write((const char *)&programSize, sizeof(k_int));
	ofs.write((const char *)bc, programSize);

	// String pool section
	const char *sp = strPool.GetPool();
	ofs.write((const char *)&strPoolSize, sizeof(k_int));
	ofs.write(sp, strPoolSize);

	ofs.close();

	bytecode.CleanUp();
	strPool.CleanUp();

	return true;
}

bool CompileAndRun()
{
	ifstream ifs(fileName.c_str(), ios::binary);
	
	if (!ifs.is_open())
	{
		cout << "*** Can not open file: " << fileName << endl;
		return false;
	}

	int len;
	ifs.seekg(0, ios::end);
	len = ifs.tellg();
	ifs.seekg(0, ios::beg);

	char *src = new char[len + 1];
	memset(src, 0, len + 1);
	ifs.read(src, len);
	ifs.close();

	if (!Compile(src))
	{
		delete[] src;
		bytecode.CleanUp();
		strPool.CleanUp();
		return false;
	}

	delete[] src;

	bool result = KVM::Run(bytecode.GetStream(), programSize, strPool.GetPool());

	bytecode.CleanUp();
	strPool.CleanUp();

	return result;
}

bool RunFile()
{
	const unsigned char *code;
	const char *pool;
	k_uint codeSize;
	k_uint poolSize;

	char magic[5] = { 0 };
	char ver[5]   = { 0 };
	char pad[8]   = { 0 };

	ifstream ifs(fileName.c_str(), ios::binary);

	if (!ifs.is_open())
	{
		cout << "Can not open file: " << fileName << endl;
		return false;
	}

	ifs.read(magic, 4);
	if (!my_strequ(magic, "kiml"))
	{
		cout << "Invalid KC file." << endl;
		return false;
	}

	ifs.read(ver, 4);
	if (!my_strequ(ver, "1101"))
	{
		cout << "Invalid KC file." << endl;
		return false;
	}

	ifs.read(pad, 8);
	for (int i = 0; i < 8; ++i)
	{
		if (pad[i])
		{
			cout << "Invalid KC file." << endl;
			return false;
		}
	}

	ifs.read((char *)&codeSize, sizeof(k_int));
	
	code = new unsigned char[codeSize];
	ifs.read((char *)code, codeSize);

	ifs.read((char *)&poolSize, sizeof(k_int));

	pool = new char[poolSize];
	ifs.read((char *)pool, poolSize);

	ifs.close();

	bool result = KVM::Run(code, codeSize, pool);

	delete[] code;
	delete[] pool;

	return result;
}

void Interact()
{
	k_string src;
	char ln[1024]; //line buffer
	int c, n;
	k_uint lno = 1;

	cout << "KimL Interactive Mode.\n"
		 << "Enter lines of code and terminate using\n\
	Ctrl+Z on a new line (Windows) or\n\
	Ctrl+D (Linux).\n"
		 << "Type '.exit' to close the program.\n\n";

	for (;;)
	{
		for (;;)
		{
			printf("%03u> ", lno);

			for (n = 0; n < 1023; ++n)
			{
				c = fgetc(stdin);
				if (c == '\n')
				{
					ln[n] = c;
					ln[n+1] = 0;
					break;
				}
				else if (c == EOF)
				{
					break;
				}

				ln[n] = c;
			}
			
			++lno;

			if (my_strequ(ln, ".exit\n"))
			{
				src.clear();
				return;
			}

			if (c == EOF /*Ctrl^Z*/)
				break;

			src.append(ln);
		}

		if (Compile(src.c_str()))
			KVM::Run(bytecode.GetStream(), programSize, strPool.GetPool());

		bytecode.CleanUp();
		strPool.CleanUp();

		src.clear();
		lno = 1;
	}
}
